
import java.security.InvalidParameterException;
import java.util.*;
import java.util.concurrent.*;

/**
 * Calculates the multiplied result of two matrices using concurrency.
 * 
 * @author Corvus, Janne
 * 
 */
public class MatrixCalculator {

    /**
     * A calculator agent to do the job of calculations.
     *
     * @author Corvus, Janne
     *
     */
    public final int ROWS_PER_WORKUNIT = 20;

    class Calculator extends Thread {

        /**
         *
         *
         * @param source_matrix_A
         *            The first matrix.
         * @param source_matrix_B
         *            The second matrix.
         */
        public Calculator(double[][] source_matrix_A, double[][] source_matrix_B) {
        }

        /**
         * Calculates a node using a simple, trivial method.
         *
         * @param y
         *            Node's y coordinate in the result matrix.
         * @param x
         *            Node's x coordinate in the result matrix.
         * @return The node's value.
         */
        private double calculateNode(int y, int x) {
            double value = 0;
            for (int i = 0; i < source_matrix_A[y].length; i++) {
                value += source_matrix_A[y][i] * source_matrix_B[i][x];
            }
            return value;
        }

        /**
         * Picks a free row no other agent is working on and calculates the
         * nodes for it.
         */
        @Override
        public synchronized void run() {
            try {
                while (true) {
                    int row = free.take();

                    workInProgress.add(row);
                    double[][] work_rows = new double[ROWS_PER_WORKUNIT][targetMatrixXSize];

                    for (int y = 0; y < ROWS_PER_WORKUNIT; y++) {
                        for (int x = 0; x < targetMatrixXSize; x++) {
                            double value = 0;
                            for (int i = 0; i < source_matrix_A[row + y].length; i++) {
                                value += source_matrix_A[row + y][i] * source_matrix_B[i][x];
                            }
                            work_rows[y][x] = value;
                        }
                    }

                    for (int y = 0; y < ROWS_PER_WORKUNIT; y++) {
                        target_matrix[row + y] = work_rows[y];
                    }
                    done.add(row);
                    workInProgress.remove(row);
                }
            } catch (InterruptedException e) {
            }
        }
    }
    static double[][] source_matrix_A;
    static double[][] source_matrix_B;
    static double[][] target_matrix;
    BlockingQueue<Integer> free;
    Set<Integer> done;
    Set<Integer> workInProgress;
    private int targetMatrixXSize;
    private int targetMatrixYSize;
    private int threads;

    /**
     * Sets everything up for calculation.
     *
     * @param a
     *            First matrix
     * @param b
     *            Second matrix
     */
    public MatrixCalculator(double[][] a, double[][] b) {
        if (a == null) {
            throw new InvalidParameterException();
        }
        if (b == null) {
            throw new InvalidParameterException();
        }

        if (a[0].length != b.length) {
            throw new InvalidParameterException("Invalid matrix sizes");
        }

        source_matrix_A = a;
        source_matrix_B = b;

        targetMatrixXSize = MatrixCalculator.source_matrix_B[0].length;
        targetMatrixYSize = MatrixCalculator.source_matrix_A.length;

        target_matrix = new double[targetMatrixYSize][targetMatrixXSize];

        done = Collections.synchronizedSet(new HashSet<Integer>());
        workInProgress = Collections.synchronizedSet(new HashSet<Integer>());

        free = new LinkedBlockingQueue<Integer>();
    }

    public int getThreads() {
        return threads;
    }

    /**
     *
     * @param threads
     *            Number of threads to use. If 0, uses one thread for each
     *            available processor.
     */
    public void setThreads(int threads) {
        if (threads == 0) {
            threads = Runtime.getRuntime().availableProcessors();
        }
        this.threads = threads;
    }

    private void createWorkunits() {
        for (int y = 0; y < targetMatrixYSize; y = y + ROWS_PER_WORKUNIT) {
            free.add(y);
        }
    }

    /**
     *
     * @return The result matrix
     */
    public double[][] calculate() {
        Calculator[] workers = new Calculator[threads];

        for (int i = 0; i < threads; i++) {
            workers[i] = new Calculator(source_matrix_A, source_matrix_B);
            workers[i].start();
        }

        // Create workunits
        createWorkunits();

        while (!free.isEmpty()) {
            try {
                Thread.sleep(10);
            } catch (InterruptedException ex) {
            }
        }

        while (!workInProgress.isEmpty()) {
            try {
                Thread.sleep(10);
            } catch (InterruptedException ex) {
            }
        }

        for (int i = 0; i < threads; i++) {
            workers[i].interrupt();
        }

        return target_matrix;
    }

    /**
     * Calculates the Matrix node's value. Trivial.
     *
     * @param y
     *            Target node's y coordinate
     * @param x
     *            Target node's x coordinate
     * @return value The target node's value
     */
    static public double calculateNode(int y, int x) {
        double value = 0;
        for (int i = 0; i < source_matrix_A[y].length; i++) {
            value += source_matrix_A[y][i] * source_matrix_B[i][x];
        }
        return value;
    }
}
